bookwiz.io / app / api / books / [id] / github-integration / compare / route.ts
route.ts
Raw
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase'

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const supabase = createServerSupabaseClient()
    const bookId = params.id
    
    // Get current user session
    const authHeader = request.headers.get('authorization')
    let user
    
    try {
      if (authHeader) {
        const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(authHeader.replace('Bearer ', ''))
        if (!authError) user = authUser
      } else {
        const { data: { user: sessionUser }, error: sessionError } = await supabase.auth.getUser()
        if (!sessionError) user = sessionUser
      }
    } catch (e) {
      // Ignore auth errors
    }

    if (!user) {
      return NextResponse.json(
        { error: 'Authentication required' },
        { status: 401 }
      )
    }

    // Get GitHub integration
    const { data: profile } = await supabase
      .from('profiles')
      .select('github_integrations')
      .eq('id', user.id)
      .single()

    const integration = profile?.github_integrations?.[bookId]
    if (!integration) {
      return NextResponse.json(
        { error: 'GitHub integration not found' },
        { status: 404 }
      )
    }

    // Get files from GitHub repository
    const owner = integration.github_username
    const repo = integration.repository_name
    const accessToken = integration.access_token

    try {
      // First, get the latest commit SHA to ensure we're comparing against the latest version
      const latestCommit = await getLatestCommit(owner, repo, accessToken)
      
      if (!latestCommit) {
        console.log('๐Ÿ“Š GitHub Compare API: No commits found - repository is empty')
        return NextResponse.json({
          committedFiles: {},
          repositoryEmpty: true
        })
      }

      console.log('๐Ÿ“Š GitHub Compare API: Latest commit:', latestCommit.sha)
      
      // Get repository contents using the tree API for better performance
      const committedFiles = await getRepositoryFilesFromTree(owner, repo, accessToken, latestCommit.sha)
      
      console.log('๐Ÿ“Š GitHub Compare API: Found committed files:', Object.keys(committedFiles).length)
      console.log('๐Ÿ“Š GitHub Compare API: File paths:', Object.keys(committedFiles))
      
      return NextResponse.json({
        committedFiles,
        latestCommitSha: latestCommit.sha,
        repositoryEmpty: false
      })
    } catch (githubError) {
      console.error('โŒ GitHub Compare API: Error fetching files:', githubError)
      // Check if it's a 409 error (empty repository)
      if (githubError instanceof Error && githubError.message.includes('409')) {
        console.log('๐Ÿ“Š GitHub Compare API: Repository is empty')
        return NextResponse.json({
          committedFiles: {},
          repositoryEmpty: true
        })
      }
      return NextResponse.json(
        { error: 'Failed to fetch files from GitHub', details: githubError instanceof Error ? githubError.message : String(githubError) },
        { status: 500 }
      )
    }

  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

// Helper function to get the latest commit
async function getLatestCommit(owner: string, repo: string, accessToken: string): Promise<{ sha: string; commit: any } | null> {
  try {
    const url = `https://api.github.com/repos/${owner}/${repo}/commits/HEAD`
    console.log(`๐Ÿ“Š GitHub Compare API: Fetching latest commit from ${url}`)
    
    const response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json'
      }
    })

    if (!response.ok) {
      if (response.status === 409) {
        // Repository is empty
        return null
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
    }

    return await response.json()
  } catch (error) {
    console.error('โŒ GitHub Compare API: Error fetching latest commit:', error)
    throw error
  }
}

// More efficient function using GitHub's tree API
async function getRepositoryFilesFromTree(owner: string, repo: string, accessToken: string, commitSha: string): Promise<{ [path: string]: string }> {
  const files: { [path: string]: string } = {}
  
  try {
    // Get the tree for the commit (recursive to get all files)
    const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${commitSha}?recursive=1`
    console.log(`๐Ÿ“Š GitHub Compare API: Fetching tree from ${treeUrl}`)
    
    const treeResponse = await fetch(treeUrl, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json'
      }
    })

    if (!treeResponse.ok) {
      throw new Error(`GitHub Tree API error: ${treeResponse.status} ${treeResponse.statusText}`)
    }

    const treeData = await treeResponse.json()
    console.log(`๐Ÿ“Š GitHub Compare API: Found ${treeData.tree.length} items in tree`)
    
    // Filter for files only (not directories) and exclude README.md
    const fileItems = treeData.tree.filter((item: any) => 
      item.type === 'blob' && 
      item.path !== 'README.md' &&
      !item.path.startsWith('.git')
    )
    
    console.log(`๐Ÿ“Š GitHub Compare API: Processing ${fileItems.length} files`)
    
    // Fetch file contents in batches to avoid rate limiting
    const batchSize = 10
    for (let i = 0; i < fileItems.length; i += batchSize) {
      const batch = fileItems.slice(i, i + batchSize)
      
      await Promise.all(batch.map(async (item: any) => {
        try {
          // Use the blob API to get file content by SHA
          const blobUrl = `https://api.github.com/repos/${owner}/${repo}/git/blobs/${item.sha}`
          const blobResponse = await fetch(blobUrl, {
            headers: {
              'Authorization': `Bearer ${accessToken}`,
              'Accept': 'application/vnd.github.v3+json'
            }
          })
          
          if (blobResponse.ok) {
            const blobData = await blobResponse.json()
            
            // GitHub returns content as base64 if it's binary or large
            let content = ''
            if (blobData.encoding === 'base64') {
              try {
                content = Buffer.from(blobData.content, 'base64').toString('utf-8')
              } catch {
                // Skip binary files
                console.log(`๐Ÿ“Š GitHub Compare API: Skipping binary file ${item.path}`)
                return
              }
            } else {
              content = blobData.content
            }
            
            // Normalize file path to match BookWiz format
            const normalizedPath = normalizeFilePath(item.path)
            files[normalizedPath] = content
            console.log(`๐Ÿ“Š GitHub Compare API: Retrieved content for ${normalizedPath} (${content.length} chars)`)
          } else {
            console.error(`โŒ GitHub Compare API: Failed to fetch blob for ${item.path}:`, blobResponse.status)
          }
        } catch (fileError) {
          console.error(`โŒ GitHub Compare API: Error fetching file ${item.path}:`, fileError)
        }
      }))
    }
  } catch (error) {
    console.error(`โŒ GitHub Compare API: Error in getRepositoryFilesFromTree:`, error)
    throw error
  }
  
  return files
}

// Helper function to normalize file paths to match BookWiz format
function normalizeFilePath(githubPath: string): string {
  // Remove any leading slashes and normalize the path
  const normalized = githubPath.replace(/^\/+/, '')
  
  // If the file is directly in the root, just return the filename
  // This matches how BookWiz stores file paths
  const parts = normalized.split('/')
  if (parts.length === 1) {
    return parts[0]
  }
  
  // For nested files, return the full path
  return normalized
}

// Keep the old function as fallback if needed
async function getRepositoryFiles(owner: string, repo: string, accessToken: string, path: string = ''): Promise<{ [path: string]: string }> {
  const files: { [path: string]: string } = {}
  
  try {
    const url = `https://api.github.com/repos/${owner}/${repo}/contents${path ? `/${path}` : ''}`
    console.log(`๐Ÿ“Š GitHub Compare API: Fetching ${url}`)
    
    const response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json'
      }
    })

    if (!response.ok) {
      if (response.status === 404) {
        // Repository might be empty or path doesn't exist
        console.log(`๐Ÿ“Š GitHub Compare API: Path not found (404): ${path || 'root'}`)
        return files
      }
      console.error(`โŒ GitHub Compare API: GitHub API error for ${url}:`, response.status, response.statusText)
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
    }

    const contents = await response.json()
    
    // Handle single file vs array of files
    const items = Array.isArray(contents) ? contents : [contents]
    console.log(`๐Ÿ“Š GitHub Compare API: Found ${items.length} items in ${path || 'root'}`)
    
    for (const item of items) {
      if (item.type === 'file' && item.name !== 'README.md') {
        try {
          // Get file content
          const fileResponse = await fetch(item.download_url, {
            headers: {
              'Authorization': `Bearer ${accessToken}`
            }
          })
          
          if (fileResponse.ok) {
            const content = await fileResponse.text()
            const filePath = path ? `${path}/${item.name}` : item.name
            files[filePath] = content
            console.log(`๐Ÿ“Š GitHub Compare API: Retrieved content for ${filePath} (${content.length} chars)`)
          } else {
            console.error(`โŒ GitHub Compare API: Failed to fetch content for ${item.name}:`, fileResponse.status)
          }
        } catch (fileError) {
          console.error(`โŒ GitHub Compare API: Error fetching file ${item.name}:`, fileError)
        }
      } else if (item.type === 'dir') {
        // Recursively get directory contents
        const dirPath = path ? `${path}/${item.name}` : item.name
        console.log(`๐Ÿ“Š GitHub Compare API: Entering directory ${dirPath}`)
        const dirFiles = await getRepositoryFiles(owner, repo, accessToken, dirPath)
        Object.assign(files, dirFiles)
      }
    }
  } catch (error) {
    console.error(`โŒ GitHub Compare API: Error in getRepositoryFiles for path ${path}:`, error)
    throw error // Re-throw instead of silently handling
  }
  
  return files
}